﻿//-----------------------------------------------------------------------
// <copyright file="AccountService.CS" company="FastNET Services">
//     Copyright (c) 2008-2009. All Rights Reserved.
// </copyright>
// <license>
//     GPLv2
// </license>
// <warranty>
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
//     BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//     DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </warranty>
//-----------------------------------------------------------------------
#region Usings

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using GateShift.Common.DataPackets;
using GateShift.Common.DataPackets.Authentication;
using GateShift.Common.Repository;
using GateShift.Servers.GameServer.LoggingSystem;
using GateShift.Services.Common;
using GateShift.Services.GameFramework.GameObjects;
using GateShift.Services.SDK.Services;
using GateShift.Services.SDK.Services.Events;
using GateShift.Services.SDK.Services.HostIntegration;
using GateShift.Services.SDK.Threading;

#endregion

namespace GateShift.Services.AccountSystem
{
    /// <summary>
    /// Game Service for handling all account related activities
    /// </summary>
    [LoadableService(typeof(IAccountService))]
    public class AccountService : ServiceBase, IAccountService, IAuthenticationClientDataHandler
    {
        #region Variables

        private readonly ThreadedDictionary<Guid, Identity> _accountsActive;
        private readonly ThreadedDictionary<Guid, byte[]> _activeChallenges;

        #endregion

        #region CTor/DTor

        /// <summary>
        /// Initializes a new instance of the AccountService class
        /// </summary>
        public AccountService()
        {
            _accountsActive = new ThreadedDictionary<Guid, Identity>();
            _activeChallenges = new ThreadedDictionary<Guid, byte[]>();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Retrieves the Account ID for a player given their Session ID
        /// </summary>
        /// <param name="sessionID">SessionID to lookup</param>
        /// <returns>Identity of the account or null if no account was found</returns>
        public Guid? GetAccountID(Guid sessionID)
        {
            return _accountsActive.ContainsKey(sessionID) ? (Guid?)_accountsActive[sessionID] : null;
        }

        /// <summary>
        /// Logs out a user with a reason defined by the system
        /// </summary>
        /// <param name="sessionID">SessionID to logout</param>
        /// <param name="description">Description as to why the system logged the user out</param>
        public void SystemLogoutSession(Guid sessionID, string description)
        {
            if (ServerHost.SessionHost.SessionExists(sessionID))
            {
                HandleLogoutRequestPacket(ServerHost.SessionHost.GetSession(sessionID), ClientLogoutResponseCode.SystemRequested, description);
            }
        }

        /// <summary>
        /// Method called to handle data from the client
        /// </summary>
        /// <param name="session">IClientSession sending the data</param>
        /// <param name="serviceCode">Service Code to handle</param>
        /// <param name="data">Data to deserialize into an IDataPacket</param>
        /// <param name="wasEncrypted">Whether or not the packet was encrypted</param>
        public void HandleClientData(IClientSession session, byte serviceCode, byte[] data, bool wasEncrypted)
        {
            // Ignore unencrypted data
            if (!wasEncrypted)
            {
                return;
            }

            // Switch based on "Packet Type"
            switch ((ServerAuthCode)serviceCode)
            {
                case ServerAuthCode.UserNameRequestPacket:
                    HandleUserNameRequestPacket(session, PacketConverter.ConvertFromByteArray<ServerUserNameRequestPacket>(data));
                    break;
                case ServerAuthCode.LoginRequestPacket:
                    HandleLoginRequestPacket(session, PacketConverter.ConvertFromByteArray<ServerLoginRequestPacket>(data));
                    break;
                case ServerAuthCode.TokenRenewalRequestPacket:
                    HandleTokenRenewalRequestPacket(session, PacketConverter.ConvertFromByteArray<ServerTokenRenewalRequestPacket>(data));
                    break;
                case ServerAuthCode.LogoutRequestPacket:
                    HandleLogoutRequestPacket(session, ClientLogoutResponseCode.ClientRequested, null);
                    break;
            }
        }

        /// <summary>
        /// Method called for any data recieved for a session that has not yet been authenticated
        /// </summary>
        /// <param name="session">IClientSession sending the data</param>
        public void HandleUnauthenticatedData(IClientSession session)
        {
            ClientValidationResponsePacket response = new ClientValidationResponsePacket
            {
                ResponseCode = ClientValidationResponseCode.RequestUserNameFirst
            };

            session.SendData((byte)ClientServiceID.AuthenticationService, (byte)ClientAuthCode.ValidationResponsePacket, PacketConverter.ConvertToByteArray(response), true);
        }

        /// <summary>
        /// Method called for when a specific IClientSession has their token expire
        /// </summary>
        /// <param name="session">IClientSession with the expired token</param>
        public void HandleExpiredToken(IClientSession session)
        {
            // If they do not check in by this point, disconnect them
            Log.AddEntry("Token Expired", string.Format("Login Session For SessionID {0} Expired", session.SessionID), LogPriorities.Debug, (int)AccountLogCategories.Authentication, TraceEventType.Verbose, (int)AccountLogEventID.TokenExpired);
            session.Disconnect();
        }

        #endregion

        #region Public Overrides

        /// <summary>
        /// Handles initialization of the service
        /// </summary>
        public override void Initialize()
        {
            ServerHost.SessionHost.RegisterClientDataHandler((byte)ServerServiceID.AuthenticationService, this);
            ServerHost.SessionHost.SessionDisconnected += SessionHost_SessionDisconnected;
            base.Initialize(); // Must call this
        }

        #endregion

        #region Private Static Methods

        /// <summary>
        /// Generates a session based random challenge ID
        /// </summary>
        /// <returns>Byte array of the challenge ID</returns>
        private static byte[] GenerateChallenge()
        {
            // We use the RNGCryptoServiceProvider to ensure our data is unique. This class is designed for cryptography randomization
            RNGCryptoServiceProvider rngProvider = new RNGCryptoServiceProvider();
            byte[] data = new byte[Properties.Settings.Default.ChallengeByteSize];
            rngProvider.GetBytes(data);
            return data;
        }

        /// <summary>
        /// Validates a hash sent by the client for the token renewal process
        /// </summary>
        /// <param name="verifyHash">Client generated hash to verify</param>
        /// <param name="sessionToken">Current session authentication token to verify</param>
        /// <param name="challenge">Session based challenge for this current session</param>
        /// <returns>Boolean based on the match of the two generated hashes</returns>
        private static bool ValidateSessionToken(byte[] verifyHash, byte[] sessionToken, byte[] challenge)
        {
            List<byte> passwordData = new List<byte>();

            // Order is going to be important for the dual hash
            passwordData.AddRange(sessionToken);
            passwordData.AddRange(challenge);

            // This is all there is to validate! SequenceEqual is provided by Linq to check equality
            return verifyHash.SequenceEqual(HashAlgorithm.Create(Properties.Settings.Default.TokenRenewalHashAlgorithm).ComputeHash(passwordData.ToArray()));
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Event handler for SessionHost notifying of a disconnected client
        /// </summary>
        /// <param name="sender">ISessionHost calling this event</param>
        /// <param name="e">ClientSessionEventArgs with information about the event</param>
        private void SessionHost_SessionDisconnected(object sender, ClientSessionEventArgs e)
        {
            _activeChallenges.Remove(e.Session.SessionID);
            _accountsActive.Remove(e.Session.SessionID);
        }

        /// <summary>
        /// Handles packets for starting and authentication session
        /// </summary>
        /// <param name="session">IClientSession on the client requesting the authorization</param>
        /// <param name="packet">ServerUserNameRequestPacket with information on the request</param>
        private void HandleUserNameRequestPacket(IClientSession session, ServerUserNameRequestPacket packet)
        {
            Account account = null;
            ClientUserNameResponsePacket response = new ClientUserNameResponsePacket();

            // Validate Username
            if (!string.IsNullOrEmpty(packet.UserName) && !Account.IsAccountNameValid(packet.UserName))
            {
                account = Account.RetrieveByUserName(packet.UserName);
            }

            Log.AddEntry("User Login Request", string.Format("Attempted login for user {0}", packet.UserName), LogPriorities.Debug, (int)AccountLogCategories.Authentication, TraceEventType.Verbose, (int)AccountLogEventID.LoginUserNameRequest);

            // Make sure we could retrieve an account
            if (account == null)
            {
                response.ResponseCode = ClientUserNameResponseCode.InvalidUser;
            }
            else
            {
                // Check that we are not locked or banned
                if (account.Locked)
                {
                    response.ResponseCode = ClientUserNameResponseCode.LockedUser;
                }
                else if (account.Banned)
                {
                    response.ResponseCode = ClientUserNameResponseCode.BannedUser;
                }
                else
                {
                    // Build the response
                    response.ResponseCode = ClientUserNameResponseCode.Success;
                    response.PasswordSalt = account.PasswordSalt;
                    response.ChallengeCode = GenerateChallenge();

                    // Add the challenge and the accountID for the session
                    _activeChallenges.Add(session.SessionID, response.ChallengeCode);
                    _accountsActive.Add(session.SessionID, account.ID);
                }
            }

            Log.AddEntry("User Login Request", string.Format("Login Session For User {0} Responding {1}", packet.UserName, response.ResponseCode), LogPriorities.Debug, (int)AccountLogCategories.Authentication, TraceEventType.Verbose, (int)AccountLogEventID.LoginUserNameResponse);

            // Send response
            session.SendData((byte)ClientServiceID.AuthenticationService, (byte)ClientAuthCode.UserNameResponsePacket, PacketConverter.ConvertToByteArray(response), true);
        }

        /// <summary>
        /// Handles the packets for a login verification
        /// </summary>
        /// <param name="session">IClientSession requesting the validation</param>
        /// <param name="packet">ServerLoginRequestPacket with information about the request</param>
        private void HandleLoginRequestPacket(IClientSession session, ServerLoginRequestPacket packet)
        {
            ClientValidationResponsePacket response = new ClientValidationResponsePacket();

            // Verify we have a session being authenticating
            if (_accountsActive.ContainsKey(session.SessionID))
            {
                // Get our account ID for the session
                Identity accountID = _accountsActive[session.SessionID];

                // Log we are verifying password
                Log.AddEntry("User Login Request", string.Format("Verifying Password for userid {0}", accountID), LogPriorities.Debug, (int)AccountLogCategories.Authentication, TraceEventType.Verbose, (int)AccountLogEventID.VerifyPasswordRequest);

                // Make sure there is an active challenge
                if (_activeChallenges.ContainsKey(session.SessionID))
                {
                    // Load the account
                    Account account = new Account(accountID);

                    // Verify not locked and validate password
                    if (account.Locked)
                    {
                        response.ResponseCode = ClientValidationResponseCode.LockedUser;
                    }

                    if (account.ValidatePassword(packet.DynamicPasswordHash, _activeChallenges[session.SessionID]))
                    {
                        response.ResponseCode = ClientValidationResponseCode.Success;

                        // Update Login Date and save
                        account.UpdateLoginDate();
                        account.ResetInvalidPasswordAttempts();
                        account.Save();

                        // Build Response
                        session.RenewSessionToken();

                        // NOTE!!! Time sensitive data
                        response.TokenLeaseTime = session.TokenExpirationLifeTime;
                        response.SessionToken = session.SessionToken;
                        response.ServerDateTime = DateTime.UtcNow;

                        Log.AddEntry("Token Issued", string.Format("User's {0} session set for renewal in {1}", account.AccountName, session.TokenExpirationLifeTime), LogPriorities.Informative, (int)AccountLogCategories.Authentication, TraceEventType.Information, (int)AccountLogEventID.TokenIssued);
                    }
                    else
                    {
                        // Since they failed, check and update the locked status based on failed password count
                        if (account.FailedPasswordAttemptCount >= Properties.Settings.Default.FailedPasswordAttemptsBeforeLockout)
                        {
                            Log.AddEntry("Account Locked", string.Format("User {0} Failed password attempt count", account.AccountName), LogPriorities.Informative, (int)AccountLogCategories.Authentication, TraceEventType.Information, (int)AccountLogEventID.PasswordUserLocked);

                            account.LockAccount(DateTime.UtcNow.Add(Properties.Settings.Default.FailedPasswordLockoutDuration));
                            account.Save();
                        }

                        response.ResponseCode = ClientValidationResponseCode.InvalidHash;
                    }
                }
                else
                {
                    response.ResponseCode = ClientValidationResponseCode.RequestUserNameFirst;
                }

                // Log result
                Log.AddEntry("User Login Request", string.Format("Verifying Password for userid {0} Responding {1}", accountID, response.ResponseCode), LogPriorities.Debug, (int)AccountLogCategories.Authentication, TraceEventType.Verbose, (int)AccountLogEventID.VerifyPasswordResponse);
            }
            else
            {
                response.ResponseCode = ClientValidationResponseCode.RequestUserNameFirst;
            }

            // Send response
            session.SendData((byte)ClientServiceID.AuthenticationService, (byte)ClientAuthCode.ValidationResponsePacket, PacketConverter.ConvertToByteArray(response), true);
        }

        /// <summary>
        /// Handles packets involved with token renewals
        /// </summary>
        /// <param name="session">IClientSession requesting the renewal</param>
        /// <param name="packet">ServerTokenRenewalRequestPacket with information on the renewal</param>
        private void HandleTokenRenewalRequestPacket(IClientSession session, ServerTokenRenewalRequestPacket packet)
        {
            ClientValidationResponsePacket response = new ClientValidationResponsePacket();

            if (_accountsActive.ContainsKey(session.SessionID))
            {
                // Get our account ID for the session
                Identity accountID = _accountsActive[session.SessionID];

                // Log we are renewing token
                Log.AddEntry("Token Renewal Request", string.Format("Renewing Token for userid {0}", accountID), LogPriorities.Debug, (int)AccountLogCategories.Authentication, TraceEventType.Verbose, (int)AccountLogEventID.TokenRenewalRequest);

                if (_activeChallenges.ContainsKey(session.SessionID))
                {
                    if (ValidateSessionToken(packet.DynamicSessionTokenHash, session.SessionToken, _activeChallenges[session.SessionID]))
                    {
                        response.ResponseCode = ClientValidationResponseCode.Success;

                        session.RenewSessionToken();

                        // NOTE!!! Time sensitive data
                        response.TokenLeaseTime = session.TokenExpirationLifeTime;
                        response.SessionToken = session.SessionToken;
                        response.ServerDateTime = DateTime.UtcNow;

                        Log.AddEntry("Token Reissued", string.Format("UserID {0} set for renewal in {1}", accountID, session.TokenExpirationLifeTime), LogPriorities.Informative, (int)AccountLogCategories.Authentication, TraceEventType.Information, (int)AccountLogEventID.TokenIssued);
                    }
                    else
                    {
                        response.ResponseCode = ClientValidationResponseCode.InvalidHash;
                    }
                }
                else
                {
                    response.ResponseCode = ClientValidationResponseCode.RequestUserNameFirst;
                }

                // Log we are renewing token
                Log.AddEntry("Token Renewal Response", string.Format("Renewing Token for userid {0} Responding {1}", accountID, response.ResponseCode), LogPriorities.Debug, (int)AccountLogCategories.Authentication, TraceEventType.Verbose, (int)AccountLogEventID.TokenRenewalRequest);
            }
            else
            {
                response.ResponseCode = ClientValidationResponseCode.RequestUserNameFirst;
            }

            session.SendData((byte)ClientServiceID.AuthenticationService, (byte)ClientAuthCode.ValidationResponsePacket, PacketConverter.ConvertToByteArray(response), true);
        }

        /// <summary>
        /// Handles packets involved with logout requests
        /// </summary>
        /// <param name="session">IClientSession requesting the renewal</param>
        /// <param name="reason">ClientLogoutResponseCode containing the reason the client is being logged out</param>
        /// <param name="description">Text to send to the client on why the client is disconnecting</param>
        private void HandleLogoutRequestPacket(IClientSession session, ClientLogoutResponseCode reason, string description)
        {
            ClientLogoutResponsePacket response = new ClientLogoutResponsePacket { ResponseCode = reason, Description = description };

            // Get our account ID for the session
            if (_accountsActive.ContainsKey(session.SessionID))
            {
                Identity accountID = _accountsActive[session.SessionID];

                _activeChallenges.Remove(session.SessionID);
                _accountsActive.Remove(session.SessionID);

                // Log we are logging out
                Log.AddEntry("Logout Request", string.Format("Logging out userid {0} for reason {1}:{2}", accountID, reason, description), LogPriorities.Debug, (int)AccountLogCategories.Authentication, TraceEventType.Verbose, (int)AccountLogEventID.LogoutResponse);
            }

            session.SendData((byte)ClientServiceID.AuthenticationService, (byte)ClientAuthCode.LogoutResponsePacket, PacketConverter.ConvertToByteArray(response), true);
        }

        #endregion
    }
}
